JavaScript 面向对象编程系列(一)创建对象

要说 JavaScript 什么最难,对象首当其冲。

对象的含义

ECMA-262 对于对象的官方定义是:

无序属性的集合,其属性可以包括基本值、对象或者函数。

对象的属性是由键对值(key: value)形式定义的,当 value 值为函数时,则称为方法。通常我们将对象的特征称之为属性,对象的行为称之为方法。

比如将我这个人作为一个对象,我拥有很多属性,姓名、性别、年龄,还有很多方法,比如能吃能睡能打麻将。

我在刚开始学 JavaScript 的时候,一直弄不清什么是构造函数、原型对象和实例,还有他们之间的关系,在总结创建对象的几种方法之前,先聊一聊这三个东西。

构造函数、原型对象、实例

定义

构造函数

构造函数就是用 new 操作符调用的普通函数,构造函数与普通函数的 唯一区别 在于:构造函数用 new 调用来创建对象,例如 Object、Array、Function等,都是构造函数。

使用构造函数的 目的 就是:所有用同一个构造函数创建的实例对象都具有同样的属性和方法。

一般构造函数名的 首字母大写 用以区别,比如下面代码中的 Person。

1
2
3
4
5
6
7
8
9
function Person(name,age) {
this.name = name;
this.age = age;
this.eat = function() {
console.log(this.name + "爱吃排骨");
}
}
var Echo = new Person("Echo",18);

实例对象

实例对象,通常简称为 实例,是通过 new 调用构造函数创建出的对象,例如上述代码中的 Echo 就是实例化出来的一个对象,实例拥有构造函数中的所有属性与方法。

原型对象

所有函数都有一个 prototype 属性,指向这个构造函数的原型对象,这个原型对象里的属性和方法是所有实例共享的。

三者的关系

函数的 prototype 属性,指向这个构造函数的原型对象。

原型对象的 constructor 属性,指向它的构造函数。

实例中的 [[prototype]] (除 IE 外的浏览器也支持 __proto__ 属性)指向原型对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function Person(name,age) {
this.name = name;
this.age = age;
this.eat = function() {
console.log(this.name + "爱吃排骨");
}
}
var Echo = new Person("Echo",18);
var Mike = new Person("Mike",20);
Echo.sex = "女";
Mike.job = "学生";
Person.prototype.sayHi = function(argument) {
console.log("Hi " + this.name);
};
Person.prototype.score = 100;
Echo.sayHi(); // Hi Echo
Echo.score = 80;
console.log(Echo.score); // 80
console.log(Mike.score); // 100
Echo.__proto__.score = 60;
console.log(Echo.score); // 80
console.log(Mike.score); // 60

在上述代码中,属性 name 和 age,方法 eat 是构造函数中定义的,实例 Echo 自有属性 sex,实例 Mike 自有属性 job,原型对象中共享属性 score 和方法 sayHi。后在实例 Echo 中添加同名自有属性 score,在调用属性时,先自有属性后共享属性。改变原型对象中的属性,所有实例的该属性同时改变。

构造函数、原型对象、实例三者关系图

检测属性

要判断对象是否具有某个属性,通常有两种方法。

in 操作符

in 操作符会检查对象自有属性和原型属性。

1
2
console.log("score" in Mike); // true
console.log("age" in Mike); // true

hasOwnProperty 方法

hasOwnProperty 方法只检查对象的自有属性。

1
2
console.log(Mike.hasOwnProperty("name")); // true
console.log(Mike.hasOwnProperty("score")); // false

创建对象的方法

字面量

创建对象最简单的方式就是字面量。

1
2
3
4
5
6
7
8
9
var Echo = {
name: "Echo",
age: 18,
eat: function() {
console.log("Echo 爱吃排骨");
}
};
console.log(Echo.__proto__); // Object

在使用字面量方式创建对象时,可以在定义对象时添加属性,也可以先创建一个空对象,后续再添加属性,属性名 可以是 变量 的形式,也可以是 字符串 形式。

1
2
3
4
5
6
var Echo = {};
Echo.name = "Echo";
Echo.age = 18;
Echo.eat = function() {
console.log("Echo 爱吃排骨");
}

优点: 创建方式简单,代码量少,可读性好。

缺点: 代码不具有复用性,比如说还有个人跟我一样的年纪跟我一样爱吃排骨,那我们又要创建一个新的对象,定义一样的属性,这样会造成代码的冗余。

通过字面量方式直接创建的实例对象,该实例的 __proto__ 属性指向 Object,即实例的原型对象为 Object 对象。

内置构造函数 Object

1
2
3
4
5
6
7
8
var Echo = new Object();
Echo.name = "Echo";
Echo.age = 18;
Echo.eat = function() {
console.log("Echo 爱吃排骨");
}
console.log(Echo.__proto__); // Object

实际上,字面量方式创建对象也是通过 Object 类型创建的,只是表示方式不一样而已。

工厂模式

工厂模式通过创建一个函数定义属性和方法,所有通过这个函数创建的实例对象,自动拥有函数中所有的属性和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function person(name,age) {
var o = new Object();
o.name = name;
o.age = age;
o.eat = function() {
console.log(o.name + "爱吃排骨");
}
return o;
}
var Echo = person("Echo",18);
console.log(Echo.age); // 18
Echo.eat(); // Echo爱吃排骨
var Mike = person("Mike",20);
console.log(Echo.__proto__); // Object
console.log(person.prototype); // Object
console.log(person.constructor); // Function

优点: 可以方便地创建多个具有相同属性和方法的对象。

缺点: 通过工厂模式创建的函数与实例,原型对象均为 Object,这样我们就无法判断实例的实际类型。

自定义构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person(name,age) {
this.name = name;
this.age = age;
this.eat = function() {
console.log(this.name + "爱吃排骨");
}
}
var Echo = new Person("Echo",18);
Echo.eat(); // Echo爱吃排骨
var Mike = new Person("Mike",20);
Mike.eat(); // Mike爱吃排骨
Mike.name = "Hello Mike";
console.log(Echo.name); // Echo
console.log(Mike.name); // Hello Mike

与工厂模式的区别:

  1. 没有在函数内显性创建对象
  2. 通过 this 对属性和方法赋值
  3. 没有 return 语句

之所以构造函数方法会与工厂模式有这些区别,关键还在于 new 操作符。

new 操作符的作用:

  1. 创建一个新的对象
  2. this 指向这个对象
  3. 往对象添加属性和方法(执行代码)
  4. 返回对象

优点: 所有用同一个构造函数创建的实例对象都具有同样的属性和方法,解决了对象识别问题。

缺点: 实际上并没有消除代码冗余,构造函数中的属性和方法会在每个实例中重新创建一遍,不同实例之间的属性和方法相互独立,这样就存在内存浪费问题,而事实上没有必要创建多个完成同一任务的方法。

原型模式

单纯使用原型对象来创建实例对象也需要创建一个构造函数,当然这个构造函数可以是空的。通过原型对象添加属性和方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Person() {
}
Person.prototype.name = "Echo";
Person.prototype.eat= function(){
console.log(this.name + "爱吃排骨");
};
var Echo = new Person();
console.log(Echo.name); // Echo
Echo.eat(); // Echo爱吃排骨
var Mike = new Person();
console.log(Mike.name); // Echo
Mike.eat(); // Echo爱吃排骨

可以看到通过原型对象定义的方法和属性是共享的。除此之外,还有一种更简洁的字面量形式定义原型对象的属性和方法。

1
2
3
4
5
6
7
8
9
10
11
12
function Person() {
}
Person.prototype = {
constructor: Person,
name: "Echo",
eat: function() {
console.log(this.name + "爱吃排骨");
}
}

使用字面量形式必须注意: 指明该原型对象的构造函数,否则实例的 __proto__ 属性指向 Object。

优点: 对于不同实例的同一个属性和方法其实都是同一个内存地址,避免了内存浪费,提高运行效率。

缺点: 共享引用类型(对象)时,一个实例的改变会影响另一个实例,我们有的时候需要这种共享性,有的时候会恼怒这种共享性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Person(name,age) {
}
Person.prototype = {
constructor: Person,
friends: ["Lily","Jack"]
}
var Echo = new Person();
var Mike = new Person();
console.log(Echo.friends); // ["Lily","Jack"]
console.log(Mike.friends); // ["Lily","Jack"]
Echo.friends.push("Lucy");
console.log(Echo.friends); // ["Lily","Jack","Lucy"]
console.log(Mike.friends); // ["Lily","Jack","Lucy"]

组合使用构造函数模式和原型模式

这种创建对象的方式是目前使用最广泛的,它结合了构造函数模式与原型模式的优点,利用构造函数定义实例属性,利用原型对象定义方法和共享属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Person(name,age) {
this.name = name;
this.age = age;
}
Person.prototype = {
constructor: Person,
sayHi: function() {
console.log("Hi " + this.name);
},
friends: ["Lily","Jack"]
}
var Echo = new Person("Echo",18);
var Mike = new Person("Mike",20);
Echo.sayHi(); // Hi Echo
Mike.sayHi(); // Hi Mike
console.log(Echo.friends); // ["Lily","Jack"]
console.log(Mike.friends); // ["Lily","Jack"]
Echo.friends.push("Lucy");
console.log(Echo.friends); // ["Lily","Jack","Lucy"]
console.log(Mike.friends); // ["Lily","Jack","Lucy"]

其他方法

在《JavaScript 高级程序设计》中还提到了三种其他创建对象的方法:动态原型模式、寄生构造函数模式、稳妥构造函数模式。由于这三种方式并不常用,因此简单了解一下即可。

动态原型模式

通过检查检查某个应该存在的方法是否有效,来决定是否需要初始化原型。

寄生构造函数模式

构造函数的内部与工厂模式相同,使用 new 调用构造函数。

稳妥构造函数模式

形式上与工厂模式相同。

Fork me on GitHub